// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/interpolated_transform.h"

#include <cmath>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#include "base/logging.h"
#include "ui/gfx/animation/tween.h"

namespace {

static const double EPSILON = 1e-6;

bool IsMultipleOfNinetyDegrees(double degrees)
{
    double remainder = fabs(fmod(degrees, 90.0));
    return remainder < EPSILON || 90.0 - remainder < EPSILON;
}

// Returns false if |degrees| is not a multiple of ninety degrees or if
// |rotation| is NULL. It does not affect |rotation| in this case. Otherwise
// *rotation is set to be the appropriate sanitized rotation matrix. That is,
// the rotation matrix corresponding to |degrees| which has entries that are all
// either 0, 1 or -1.
bool MassageRotationIfMultipleOfNinetyDegrees(gfx::Transform* rotation,
    float degrees)
{
    if (!IsMultipleOfNinetyDegrees(degrees) || !rotation)
        return false;

    gfx::Transform transform;
    SkMatrix44& m = transform.matrix();
    float degrees_by_ninety = degrees / 90.0f;

    int n = static_cast<int>(degrees_by_ninety > 0
            ? floor(degrees_by_ninety + 0.5f)
            : ceil(degrees_by_ninety - 0.5f));

    n %= 4;
    if (n < 0)
        n += 4;

    // n should now be in the range [0, 3]
    if (n == 1) {
        m.set3x3(0, 1, 0,
            -1, 0, 0,
            0, 0, 1);
    } else if (n == 2) {
        m.set3x3(-1, 0, 0,
            0, -1, 0,
            0, 0, 1);
    } else if (n == 3) {
        m.set3x3(0, -1, 0,
            1, 0, 0,
            0, 0, 1);
    }

    *rotation = transform;
    return true;
}

} // namespace

namespace ui {

///////////////////////////////////////////////////////////////////////////////
// InterpolatedTransform
//

InterpolatedTransform::InterpolatedTransform()
    : start_time_(0.0f)
    , end_time_(1.0f)
    , reversed_(false)
{
}

InterpolatedTransform::InterpolatedTransform(float start_time,
    float end_time)
    : start_time_(start_time)
    , end_time_(end_time)
    , reversed_(false)
{
}

InterpolatedTransform::~InterpolatedTransform() { }

gfx::Transform InterpolatedTransform::Interpolate(float t) const
{
    if (reversed_)
        t = 1.0f - t;
    gfx::Transform result = InterpolateButDoNotCompose(t);
    if (child_.get()) {
        result.ConcatTransform(child_->Interpolate(t));
    }
    return result;
}

void InterpolatedTransform::SetChild(InterpolatedTransform* child)
{
    child_.reset(child);
}

inline float InterpolatedTransform::ValueBetween(float time,
    float start_value,
    float end_value) const
{
    // can't handle NaN
    DCHECK(time == time && start_time_ == start_time_ && end_time_ == end_time_);
    if (time != time || start_time_ != start_time_ || end_time_ != end_time_)
        return start_value;

    // Ok if equal -- we'll get a step function. Note: if end_time_ ==
    // start_time_ == x, then if none of the numbers are NaN, then it
    // must be true that time < x or time >= x, so we will return early
    // due to one of the following if statements.
    DCHECK(end_time_ >= start_time_);

    if (time < start_time_)
        return start_value;

    if (time >= end_time_)
        return end_value;

    float t = (time - start_time_) / (end_time_ - start_time_);
    return static_cast<float>(
        gfx::Tween::DoubleValueBetween(t, start_value, end_value));
}

///////////////////////////////////////////////////////////////////////////////
// InterpolatedRotation
//

InterpolatedRotation::InterpolatedRotation(float start_degrees,
    float end_degrees)
    : InterpolatedTransform()
    , start_degrees_(start_degrees)
    , end_degrees_(end_degrees)
{
}

InterpolatedRotation::InterpolatedRotation(float start_degrees,
    float end_degrees,
    float start_time,
    float end_time)
    : InterpolatedTransform(start_time, end_time)
    , start_degrees_(start_degrees)
    , end_degrees_(end_degrees)
{
}

InterpolatedRotation::~InterpolatedRotation() { }

gfx::Transform InterpolatedRotation::InterpolateButDoNotCompose(float t) const
{
    gfx::Transform result;
    float interpolated_degrees = ValueBetween(t, start_degrees_, end_degrees_);
    result.Rotate(interpolated_degrees);
    if (t == 0.0f || t == 1.0f)
        MassageRotationIfMultipleOfNinetyDegrees(&result, interpolated_degrees);
    return result;
}

///////////////////////////////////////////////////////////////////////////////
// InterpolatedAxisAngleRotation
//

InterpolatedAxisAngleRotation::InterpolatedAxisAngleRotation(
    const gfx::Vector3dF& axis,
    float start_degrees,
    float end_degrees)
    : InterpolatedTransform()
    , axis_(axis)
    , start_degrees_(start_degrees)
    , end_degrees_(end_degrees)
{
}

InterpolatedAxisAngleRotation::InterpolatedAxisAngleRotation(
    const gfx::Vector3dF& axis,
    float start_degrees,
    float end_degrees,
    float start_time,
    float end_time)
    : InterpolatedTransform(start_time, end_time)
    , axis_(axis)
    , start_degrees_(start_degrees)
    , end_degrees_(end_degrees)
{
}

InterpolatedAxisAngleRotation::~InterpolatedAxisAngleRotation() { }

gfx::Transform
InterpolatedAxisAngleRotation::InterpolateButDoNotCompose(float t) const
{
    gfx::Transform result;
    result.RotateAbout(axis_, ValueBetween(t, start_degrees_, end_degrees_));
    return result;
}

///////////////////////////////////////////////////////////////////////////////
// InterpolatedScale
//

InterpolatedScale::InterpolatedScale(float start_scale, float end_scale)
    : InterpolatedTransform()
    , start_scale_(gfx::Point3F(start_scale, start_scale, start_scale))
    , end_scale_(gfx::Point3F(end_scale, end_scale, end_scale))
{
}

InterpolatedScale::InterpolatedScale(float start_scale, float end_scale,
    float start_time, float end_time)
    : InterpolatedTransform(start_time, end_time)
    , start_scale_(gfx::Point3F(start_scale, start_scale, start_scale))
    , end_scale_(gfx::Point3F(end_scale, end_scale, end_scale))
{
}

InterpolatedScale::InterpolatedScale(const gfx::Point3F& start_scale,
    const gfx::Point3F& end_scale)
    : InterpolatedTransform()
    , start_scale_(start_scale)
    , end_scale_(end_scale)
{
}

InterpolatedScale::InterpolatedScale(const gfx::Point3F& start_scale,
    const gfx::Point3F& end_scale,
    float start_time,
    float end_time)
    : InterpolatedTransform(start_time, end_time)
    , start_scale_(start_scale)
    , end_scale_(end_scale)
{
}

InterpolatedScale::~InterpolatedScale() { }

gfx::Transform InterpolatedScale::InterpolateButDoNotCompose(float t) const
{
    gfx::Transform result;
    float scale_x = ValueBetween(t, start_scale_.x(), end_scale_.x());
    float scale_y = ValueBetween(t, start_scale_.y(), end_scale_.y());
    float scale_z = ValueBetween(t, start_scale_.z(), end_scale_.z());
    result.Scale3d(scale_x, scale_y, scale_z);
    return result;
}

///////////////////////////////////////////////////////////////////////////////
// InterpolatedTranslation
//

InterpolatedTranslation::InterpolatedTranslation(const gfx::PointF& start_pos,
    const gfx::PointF& end_pos)
    : InterpolatedTransform()
    , start_pos_(start_pos)
    , end_pos_(end_pos)
{
}

InterpolatedTranslation::InterpolatedTranslation(const gfx::PointF& start_pos,
    const gfx::PointF& end_pos,
    float start_time,
    float end_time)
    : InterpolatedTransform(start_time, end_time)
    , start_pos_(start_pos)
    , end_pos_(end_pos)
{
}

InterpolatedTranslation::InterpolatedTranslation(const gfx::Point3F& start_pos,
    const gfx::Point3F& end_pos)
    : InterpolatedTransform()
    , start_pos_(start_pos)
    , end_pos_(end_pos)
{
}

InterpolatedTranslation::InterpolatedTranslation(const gfx::Point3F& start_pos,
    const gfx::Point3F& end_pos,
    float start_time,
    float end_time)
    : InterpolatedTransform(start_time, end_time)
    , start_pos_(start_pos)
    , end_pos_(end_pos)
{
}

InterpolatedTranslation::~InterpolatedTranslation() { }

gfx::Transform
InterpolatedTranslation::InterpolateButDoNotCompose(float t) const
{
    gfx::Transform result;
    result.Translate3d(ValueBetween(t, start_pos_.x(), end_pos_.x()),
        ValueBetween(t, start_pos_.y(), end_pos_.y()),
        ValueBetween(t, start_pos_.z(), end_pos_.z()));
    return result;
}

///////////////////////////////////////////////////////////////////////////////
// InterpolatedConstantTransform
//

InterpolatedConstantTransform::InterpolatedConstantTransform(
    const gfx::Transform& transform)
    : InterpolatedTransform()
    , transform_(transform)
{
}

gfx::Transform
InterpolatedConstantTransform::InterpolateButDoNotCompose(float t) const
{
    return transform_;
}

InterpolatedConstantTransform::~InterpolatedConstantTransform() { }

///////////////////////////////////////////////////////////////////////////////
// InterpolatedTransformAboutPivot
//

InterpolatedTransformAboutPivot::InterpolatedTransformAboutPivot(
    const gfx::Point& pivot,
    InterpolatedTransform* transform)
    : InterpolatedTransform()
{
    Init(pivot, transform);
}

InterpolatedTransformAboutPivot::InterpolatedTransformAboutPivot(
    const gfx::Point& pivot,
    InterpolatedTransform* transform,
    float start_time,
    float end_time)
    : InterpolatedTransform()
{
    Init(pivot, transform);
}

InterpolatedTransformAboutPivot::~InterpolatedTransformAboutPivot() { }

gfx::Transform
InterpolatedTransformAboutPivot::InterpolateButDoNotCompose(float t) const
{
    if (transform_.get()) {
        return transform_->Interpolate(t);
    }
    return gfx::Transform();
}

void InterpolatedTransformAboutPivot::Init(const gfx::Point& pivot,
    InterpolatedTransform* xform)
{
    gfx::Transform to_pivot;
    gfx::Transform from_pivot;
    to_pivot.Translate(SkIntToMScalar(-pivot.x()), SkIntToMScalar(-pivot.y()));
    from_pivot.Translate(SkIntToMScalar(pivot.x()), SkIntToMScalar(pivot.y()));

    scoped_ptr<InterpolatedTransform> pre_transform(
        new InterpolatedConstantTransform(to_pivot));
    scoped_ptr<InterpolatedTransform> post_transform(
        new InterpolatedConstantTransform(from_pivot));

    pre_transform->SetChild(xform);
    xform->SetChild(post_transform.release());
    transform_.reset(pre_transform.release());
}

InterpolatedMatrixTransform::InterpolatedMatrixTransform(
    const gfx::Transform& start_transform,
    const gfx::Transform& end_transform)
    : InterpolatedTransform()
{
    Init(start_transform, end_transform);
}

InterpolatedMatrixTransform::InterpolatedMatrixTransform(
    const gfx::Transform& start_transform,
    const gfx::Transform& end_transform,
    float start_time,
    float end_time)
    : InterpolatedTransform()
{
    Init(start_transform, end_transform);
}

InterpolatedMatrixTransform::~InterpolatedMatrixTransform() { }

gfx::Transform
InterpolatedMatrixTransform::InterpolateButDoNotCompose(float t) const
{
    gfx::DecomposedTransform blended;
    bool success = gfx::BlendDecomposedTransforms(&blended,
        end_decomp_,
        start_decomp_,
        t);
    DCHECK(success);
    return gfx::ComposeTransform(blended);
}

void InterpolatedMatrixTransform::Init(const gfx::Transform& start_transform,
    const gfx::Transform& end_transform)
{
    bool success = gfx::DecomposeTransform(&start_decomp_, start_transform);
    DCHECK(success);
    success = gfx::DecomposeTransform(&end_decomp_, end_transform);
    DCHECK(success);
}

} // namespace ui
